package study.concurrent.lock.support.redis;

import cn.hutool.core.thread.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import study.concurrent.lock.support.utils.LogUtils;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RedisLock {

    private static final String EMPTY = "";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public ValueOperations<String, String> string() {
        return redisTemplate.opsForValue();
    }

    /**
     * EXISTS + SET
     * （1）判断锁是否存在
     * （2）锁不存在时，加锁
     * ----------
     * 【产生问题】
     * （1）多进程可同时执行 EXISTS，无法满足锁的互斥性（EXISTS+SET不具备原子性）
     */
    public boolean tryLock(String lockKey) {
        //判断锁是否存在
        Boolean has = redisTemplate.hasKey(lockKey);
        log.info("锁 {} 是否存在: {}", lockKey, has);
        //
        //多进程执行 EXISTS 结果一样时，无法满足互斥性
        //
        boolean acquired;
        if (!has) { //不存在时加锁
            string().set(lockKey, EMPTY);
            acquired = true;
            LogUtils.acquired(lockKey);
        } else {
            acquired = false;
            LogUtils.notAcquired(lockKey);
        }
        return acquired;
    }

    /**
     * SET NX（锁是否存在，不存在就加锁）
     * ----------
     * 【解决问题】
     * （1）满足锁的互斥性
     * ----------
     * 【产生问题】
     * （1）获取锁后，释放锁之前程序或机器 crash，产生死锁
     */
    public boolean tryLockV1(String lockKey) {
        boolean acquired;
        if (string().setIfAbsent(lockKey, EMPTY)) { //加锁，原子
            acquired = true;
            LogUtils.acquired(lockKey);
        } else {
            acquired = false;
            LogUtils.notAcquired(lockKey);
        }
        return acquired;
    }

    /**
     * SET EX NX（锁是否存在，不存在就加锁，并设置失效时间）
     * ----------
     * 【解决问题】
     * （1）一定程度上解决了死锁问题（锁过期值的选择不好把握）
     * ----------
     * 【产生问题】
     * （1）过期时间 < 实际执行时间，无法满足互斥性（临界区未严格串行执行）
     * （2）过期时间 > 实际执行时间，
     */
    public boolean tryLockV2(String lockKey, long expire) {
        boolean acquired;
        if (string().setIfAbsent(lockKey, EMPTY, expire, TimeUnit.MILLISECONDS)) {
            acquired = true;
            LogUtils.acquired(lockKey);
        } else {
            acquired = false;
            LogUtils.notAcquired(lockKey);
        }
        return acquired;
    }

    /**
     * ----------
     * 【解决问题】
     * （1）
     * ----------
     * 【产生问题】
     * （1）
     */
    public boolean tryLockV3(String lockKey, String clientId, long expire) {
        boolean acquired;
        if (string().setIfAbsent(lockKey, clientId, expire, TimeUnit.MILLISECONDS)) {
            acquired = true;
            LogUtils.acquired(lockKey);
        } else {
            acquired = false;
            LogUtils.notAcquired(lockKey);
        }
        return acquired;
    }

    public boolean lock(String lockKey) {
        boolean succ = string().setIfAbsent(lockKey, EMPTY);
        //自旋
        while (!succ) {
            ThreadUtil.sleep(100);
            succ = string().setIfAbsent(lockKey, EMPTY);
        }
        return succ;
    }

    /**
     * DELETE
     * ----------
     * 【产生问题】
     * （1）B 进程删除了 A 进程的锁
     */
    public boolean unlock(String lockKey) {
        int retry = 0;
        boolean succ = false;
        while (!succ && retry < 3) {
            succ = redisTemplate.delete(lockKey);
            log.info("△△△ 第 {} 次释放锁: {}", retry, succ);
            ThreadUtil.sleep(500);
            retry++;
        }
        return succ;
    }

    /**
     * GET + DELETE
     * ----------
     * 【解决问题】
     * （1）
     * ----------
     * 【产生问题】
     * （1）client id判断和删除不具有原子性
     */
    public boolean unlock(String lockKey, String clientId) {
        String value = string().get(lockKey);
        if (!Objects.equals(value, clientId)) {
            log.warn("释放锁 client id {} 和实际 client id {} 不一致", value, clientId);
            return false;
        }
        boolean succ = redisTemplate.delete(lockKey);
        return unlock(lockKey);
    }

    /**
     * lua 脚本
     * ----------
     * 【解决问题】
     * （1）
     */
    public boolean unlockByLua(String lockKey, String clientId) {

        return true;
    }
}
